/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.user.server.rpc.impl;

import com.google.gwt.user.client.rpc.CustomFieldSerializer;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamReader;
import com.google.gwt.user.server.Base64Utils;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.SerializationPolicy;
import com.google.gwt.user.server.rpc.SerializationPolicyProvider;

import hr.ecs.gwt.util.PayloadDeserializer;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

/**
 * For internal use only. Used for server call serialization. This class is
 * carefully matched with the client-side version.
 */
public class ServerSerializationStreamReader extends AbstractSerializationStreamReader {

	/**
	 * Used to accumulate elements while deserializing array types. The generic
	 * type of the BoundedList will vary from the component type of the array it
	 * is intended to create when the array is of a primitive type.
	 * 
	 * @param <T> The type of object used to hold the data in the buffer
	 */
	private static class BoundedList<T> extends LinkedList<T> {
		/**
		 * 
		 */
		private static final long serialVersionUID = 947808434031682166L;
		private final Class<?> componentType;
		private final int expectedSize;

		public BoundedList(Class<?> componentType, int expectedSize) {
			this.componentType = componentType;
			this.expectedSize = expectedSize;
		}

		@Override
		public boolean add(T o) {
			assert size() < getExpectedSize();
			return super.add(o);
		}

		public Class<?> getComponentType() {
			return componentType;
		}

		public int getExpectedSize() {
			return expectedSize;
		}
	}

	/**
	 * Enumeration used to provided typed instance readers.
	 */
	private enum ValueReader {
		BOOLEAN {
			@Override
			Object readValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readBoolean();
			}
		},
		BYTE {
			@Override
			Object readValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readByte();
			}
		},
		CHAR {
			@Override
			Object readValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readChar();
			}
		},
		DOUBLE {
			@Override
			Object readValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readDouble();
			}
		},
		FLOAT {
			@Override
			Object readValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readFloat();
			}
		},
		INT {
			@Override
			Object readValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readInt();
			}
		},
		LONG {
			@Override
			Object readValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readLong();
			}
		},
		OBJECT {
			@Override
			Object readValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readObject();
			}
		},
		SHORT {
			@Override
			Object readValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readShort();
			}
		},
		STRING {
			@Override
			Object readValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readString();
			}
		};

		abstract Object readValue(ServerSerializationStreamReader stream) throws SerializationException;
	}

	/**
	 * Enumeration used to provided typed instance readers for vectors.
	 */
	private enum VectorReader {
		BOOLEAN_VECTOR {
			@Override
			protected Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readBoolean();
			}

			@Override
			protected void setSingleValue(Object array, int index, Object value) {
				Array.setBoolean(array, index, (Boolean) value);
			}
		},
		BYTE_VECTOR {
			@Override
			protected Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readByte();
			}

			@Override
			protected void setSingleValue(Object array, int index, Object value) {
				Array.setByte(array, index, (Byte) value);
			}
		},
		CHAR_VECTOR {
			@Override
			protected Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readChar();
			}

			@Override
			protected void setSingleValue(Object array, int index, Object value) {
				Array.setChar(array, index, (Character) value);
			}
		},
		DOUBLE_VECTOR {
			@Override
			protected Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readDouble();
			}

			@Override
			protected void setSingleValue(Object array, int index, Object value) {
				Array.setDouble(array, index, (Double) value);
			}
		},
		FLOAT_VECTOR {
			@Override
			protected Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readFloat();
			}

			@Override
			protected void setSingleValue(Object array, int index, Object value) {
				Array.setFloat(array, index, (Float) value);
			}
		},
		INT_VECTOR {
			@Override
			protected Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readInt();
			}

			@Override
			protected void setSingleValue(Object array, int index, Object value) {
				Array.setInt(array, index, (Integer) value);
			}
		},
		LONG_VECTOR {
			@Override
			protected Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readLong();
			}

			@Override
			protected void setSingleValue(Object array, int index, Object value) {
				Array.setLong(array, index, (Long) value);
			}
		},
		OBJECT_VECTOR {
			@Override
			protected Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readObject();
			}

			@Override
			protected void setSingleValue(Object array, int index, Object value) {
				Array.set(array, index, value);
			}
		},
		SHORT_VECTOR {
			@Override
			protected Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readShort();
			}

			@Override
			protected void setSingleValue(Object array, int index, Object value) {
				Array.setShort(array, index, (Short) value);
			}
		},
		STRING_VECTOR {
			@Override
			protected Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException {
				return stream.readString();
			}

			@Override
			protected void setSingleValue(Object array, int index, Object value) {
				Array.set(array, index, value);
			}
		};

		protected abstract Object readSingleValue(ServerSerializationStreamReader stream) throws SerializationException;

		protected abstract void setSingleValue(Object array, int index, Object value);

		/**
		 * Convert a BoundedList to an array of the correct type. This
		 * implementation consumes the BoundedList.
		 */
		protected Object toArray(Class<?> componentType, BoundedList<Object> buffer) throws SerializationException {
			if (buffer.getExpectedSize() != buffer.size()) {
				throw new SerializationException(
						"Inconsistent number of elements received. Received "
						+ buffer.size() + " but expecting " + buffer.getExpectedSize());
			}

			Object arr = Array.newInstance(componentType, buffer.size());

			for (int i = 0, n = buffer.size(); i < n; i++) {
				setSingleValue(arr, i, buffer.removeFirst());
			}

			return arr;
		}

		Object read(ServerSerializationStreamReader stream, BoundedList<Object> instance) throws SerializationException {
			for (int i = 0, n = instance.getExpectedSize(); i < n; ++i) {
				instance.add(readSingleValue(stream));
			}

			return toArray(instance.getComponentType(), instance);
		}
	}

	/**
	 * Map of {@link Class} objects to {@link ValueReader}s.
	 */
	private static final Map<Class<?>, ValueReader> CLASS_TO_VALUE_READER = new IdentityHashMap<Class<?>, ValueReader>();

	/**
	 * Map of {@link Class} objects to {@link VectorReader}s.
	 */
	private static final Map<Class<?>, VectorReader> CLASS_TO_VECTOR_READER = new IdentityHashMap<Class<?>, VectorReader>();

	private final ClassLoader classLoader;

	private SerializationPolicy serializationPolicy = RPC.getDefaultSerializationPolicy();

	private final SerializationPolicyProvider serializationPolicyProvider;

	/**
	 * Used to look up setter methods of the form 'void Class.setXXX(T value)' given a
	 * Class type and a field name corresponding to a field of type T
	 */
	private final Map<Class<?>, Map<String, Method>> settersByClass = new HashMap<Class<?>, Map<String, Method>>();

	private String[] stringTable;

	private final ArrayList<String> tokenList = new ArrayList<String>();
	private int tokenListIndex;

	private boolean skipPrintingValue;

	{
		CLASS_TO_VECTOR_READER.put(boolean[].class, VectorReader.BOOLEAN_VECTOR);
		CLASS_TO_VECTOR_READER.put(byte[].class, VectorReader.BYTE_VECTOR);
		CLASS_TO_VECTOR_READER.put(char[].class, VectorReader.CHAR_VECTOR);
		CLASS_TO_VECTOR_READER.put(double[].class, VectorReader.DOUBLE_VECTOR);
		CLASS_TO_VECTOR_READER.put(float[].class, VectorReader.FLOAT_VECTOR);
		CLASS_TO_VECTOR_READER.put(int[].class, VectorReader.INT_VECTOR);
		CLASS_TO_VECTOR_READER.put(long[].class, VectorReader.LONG_VECTOR);
		CLASS_TO_VECTOR_READER.put(Object[].class, VectorReader.OBJECT_VECTOR);
		CLASS_TO_VECTOR_READER.put(short[].class, VectorReader.SHORT_VECTOR);
		CLASS_TO_VECTOR_READER.put(String[].class, VectorReader.STRING_VECTOR);

		CLASS_TO_VALUE_READER.put(boolean.class, ValueReader.BOOLEAN);
		CLASS_TO_VALUE_READER.put(byte.class, ValueReader.BYTE);
		CLASS_TO_VALUE_READER.put(char.class, ValueReader.CHAR);
		CLASS_TO_VALUE_READER.put(double.class, ValueReader.DOUBLE);
		CLASS_TO_VALUE_READER.put(float.class, ValueReader.FLOAT);
		CLASS_TO_VALUE_READER.put(int.class, ValueReader.INT);
		CLASS_TO_VALUE_READER.put(long.class, ValueReader.LONG);
		CLASS_TO_VALUE_READER.put(Object.class, ValueReader.OBJECT);
		CLASS_TO_VALUE_READER.put(short.class, ValueReader.SHORT);
		CLASS_TO_VALUE_READER.put(String.class, ValueReader.STRING);
	}

	public ServerSerializationStreamReader(ClassLoader classLoader, SerializationPolicyProvider serializationPolicyProvider) {
		this.classLoader = classLoader;
		this.serializationPolicyProvider = serializationPolicyProvider;
	}

	public Object deserializeValue(Class<?> type) throws SerializationException {
		Object tmpObj;

		ValueReader valueReader = CLASS_TO_VALUE_READER.get(type);
		if (valueReader != null) {

			tmpObj = valueReader.readValue(this);

			PayloadDeserializer.appendToFile("\t" + type.getSimpleName());
			if(!skipPrintingValue){
				PayloadDeserializer.appendToFile("\t" + tmpObj);
			}

			return tmpObj;
		} else {
			// Arrays of primitive or reference types need to go through readObject.
			tmpObj = ValueReader.OBJECT.readValue(this);

			if(tmpObj == null){
				PayloadDeserializer.appendToFile("\t" + type.getSimpleName() + "\tnull");
			}

			return tmpObj;
		}
	}

	public int getNumberOfTokens() {
		return tokenList.size();
	}

	public SerializationPolicy getSerializationPolicy() {
		return serializationPolicy;
	}

	@Override
	public void prepareToRead(String encodedTokens) throws SerializationException {
		tokenList.clear();
		tokenListIndex = 0;
		stringTable = null;

		int idx = 0, nextIdx;
		while (-1 != (nextIdx = encodedTokens.indexOf(RPC_SEPARATOR_CHAR, idx))) {
			String current = encodedTokens.substring(idx, nextIdx);
			tokenList.add(current);
			idx = nextIdx + 1;
		}

		super.prepareToRead(encodedTokens);

		// Check the RPC version number sent by the client
		if (getVersion() < SERIALIZATION_STREAM_MIN_VERSION || getVersion() > SERIALIZATION_STREAM_VERSION) {
			throw new IncompatibleRemoteServiceException("Expecting version between "
					+ SERIALIZATION_STREAM_MIN_VERSION + " and "
					+ SERIALIZATION_STREAM_VERSION + " from client, got " + getVersion()
					+ ".");
		}

		// Check the flags
		if (!areFlagsValid()) {
			throw new IncompatibleRemoteServiceException("Got an unknown flag from " + "client: " + getFlags());
		}

		// Read the type name table
		deserializeStringTable();

		// Write the serialization policy info
		String moduleBaseURL = readString();		
		String strongName = readString();

		if (serializationPolicyProvider != null) {
			serializationPolicy = serializationPolicyProvider.getSerializationPolicy(moduleBaseURL, strongName);

			if (serializationPolicy == null) {
				throw new NullPointerException("serializationPolicyProvider.getSerializationPolicy()");
			}
		}
	}

	public boolean readBoolean() throws SerializationException {
		return !extract().equals("0");
	}

	public byte readByte() throws SerializationException {
		String value = extract();
		try {
			return Byte.parseByte(value);
		} catch (NumberFormatException e) {
			throw getNumberFormatException(value, "byte", Byte.MIN_VALUE, Byte.MAX_VALUE);
		}
	}

	public char readChar() throws SerializationException {
		// just use an int, it's more foolproof
		return (char) readInt();
	}

	public double readDouble() throws SerializationException {
		return Double.parseDouble(extract());
	}

	public float readFloat() throws SerializationException {
		return (float) Double.parseDouble(extract());
	}

	public int readInt() throws SerializationException {
		String value = extract();
		try {
			return Integer.parseInt(value);
		} catch (NumberFormatException e) {
			throw getNumberFormatException(value, "int", Integer.MIN_VALUE, Integer.MAX_VALUE);
		}
	}

	public int peekInt() throws SerializationException {
		String value = peek();
		try {
			return Integer.parseInt(value);
		} catch (NumberFormatException e) {
			throw getNumberFormatException(value, "int", Integer.MIN_VALUE, Integer.MAX_VALUE);
		}
	}

	public long readLong() throws SerializationException {
		if (getVersion() == SERIALIZATION_STREAM_MIN_VERSION) {
			return (long) readDouble() + (long) readDouble();
		} else {
			return Base64Utils.longFromBase64(extract());
		}
	}

	public short readShort() throws SerializationException {
		String value = extract();
		try {
			return Short.parseShort(value);
		} catch (NumberFormatException e) {
			throw getNumberFormatException(value, "short", Short.MIN_VALUE, Short.MAX_VALUE);
		}
	}

	public String readString() throws SerializationException {
		int tmpInt = readInt();
		return getString(tmpInt);
	}

	public String peekString() throws SerializationException {
		return getString(peekInt());
	}

	@Override
	protected Object deserialize(String typeSignature) throws SerializationException {
		Object instance = null;
		//		PayloadDeserializer.appendToFile("\n************* starting to deserialize " + typeSignature + "***************\n");
		PayloadDeserializer.appendToFile("\t" + typeSignature + "\t");
		try {
			Class<?> instanceClass;
			if (hasFlags(FLAG_ELIDE_TYPE_NAMES)) {
				if (getSerializationPolicy() instanceof TypeNameObfuscator) {
					TypeNameObfuscator obfuscator = (TypeNameObfuscator) getSerializationPolicy();
					String instanceClassName = obfuscator.getClassNameForTypeId(typeSignature);
					instanceClass = Class.forName(instanceClassName, false, classLoader);
				} else {
					throw new SerializationException(
							"The GWT module was compiled with RPC type name elision enabled, but "
							+ getSerializationPolicy().getClass().getName()
							+ " does not implement " + TypeNameObfuscator.class.getName());
				}
			} else {

				SerializedInstanceReference serializedInstRef = SerializabilityUtil.decodeSerializedInstanceReference(typeSignature);
				instanceClass = Class.forName(serializedInstRef.getName(), false, classLoader);

				// compare type signatures (client and server types has to be of the same version)
				validateTypeVersions(instanceClass, serializedInstRef);
			}

			assert (serializationPolicy != null);

			// If application server is not accessible -> whitelist wont be loaded -> skip validation 
			if(PayloadDeserializer.IS_SERVER_ACCESSIBLE){
				serializationPolicy.validateDeserialize(instanceClass);
			}

			Class<?> customSerializer = SerializabilityUtil.hasCustomFieldSerializer(instanceClass);

			int index = reserveDecodedObjectIndex();

			instance = instantiate(customSerializer, instanceClass);

			rememberDecodedObject(index, instance);

			Object replacement = deserializeImpl(customSerializer, instanceClass, instance);

			// It's possible that deserializing an object requires the original proxy
			// object to be replaced.
			if (instance != replacement) {
				rememberDecodedObject(index, replacement);
				instance = replacement;
			}

			return instance;

		} catch (ClassNotFoundException e) {
			throw new SerializationException(e);
		} catch (InstantiationException e) {
			throw new SerializationException(e);
		} catch (IllegalAccessException e) {
			throw new SerializationException(e);
		} catch (IllegalArgumentException e) {
			throw new SerializationException(e);
		} catch (InvocationTargetException e) {
			throw new SerializationException(e.getTargetException());
		} catch (NoSuchMethodException e) {
			throw new SerializationException(e);
		}
	}

	@Override
	protected String getString(int index) {
		if (index == 0) {
			return null;
		}
		// index is 1-based
		assert (index > 0);
		assert (index <= stringTable.length);
		return stringTable[index - 1];
	}

	/**
	 * Deserialize an instance that is an array. Will default to deserializing as
	 * an Object vector if the instance is not a primitive vector.
	 * 
	 * @param instanceClass
	 * @param instance
	 * @throws SerializationException
	 */
	@SuppressWarnings("unchecked")
	private Object deserializeArray(Class<?> instanceClass, Object instance) throws SerializationException {
		assert (instanceClass.isArray());

		BoundedList<Object> buffer = (BoundedList<Object>) instance;
		VectorReader instanceReader = CLASS_TO_VECTOR_READER.get(instanceClass);
		if (instanceReader != null) {
			return instanceReader.read(this, buffer);
		} else {
			return VectorReader.OBJECT_VECTOR.read(this, buffer);
		}
	}

	private void deserializeClass(Class<?> instanceClass, Object instance) 
	throws SerializationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException {
		/**
		 * A map from field names to corresponding setter methods. The reference
		 * will be null for classes that do not require special handling for
		 * server-only fields.
		 */
		Map<String, Method> setters = null;
		//		PayloadDeserializer.appendToFile("token index = " + tokenListIndex + "\n");

		/**
		 * A list of fields of this class known to the client. If null, assume the class is not
		 * enhanced and don't attempt to deal with server-only fields.
		 */
		Set<String> clientFieldNames = serializationPolicy.getClientFieldNamesForEnhancedClass(instanceClass);
		if (clientFieldNames != null) {
			// Read and set server-only instance fields encoded in the RPC data
			try {
				String encodedData = readString();
				System.out.println("encodedData = " + encodedData);
				if (encodedData != null) {
					byte[] serializedData = Base64Utils.fromBase64(encodedData);
					ByteArrayInputStream baos = new ByteArrayInputStream(serializedData);
					ObjectInputStream ois = new ObjectInputStream(baos);

					int count = ois.readInt();
					System.out.println("Base64 encoded count = " + count);
					for (int i = 0; i < count; i++) {
						String fieldName = (String) ois.readObject();
						Object fieldValue = ois.readObject();
						Field field = instanceClass.getDeclaredField(fieldName);
						field.setAccessible(true);
						field.set(instance, fieldValue);
					}
				}
			} catch (IOException e) {
				throw new SerializationException(e);
			} catch (NoSuchFieldException e) {
				throw new SerializationException(e);
			}

			setters = getSetters(instanceClass);
		}

		Field[] serializableFields = SerializabilityUtil.applyFieldSerializationPolicy(instanceClass);
		for (Field declField : serializableFields) {
			assert (declField != null);
			if ((clientFieldNames != null) && !clientFieldNames.contains(declField.getName())) {
				continue;
			}

			String s = declField.getDeclaringClass().getName();

			skipPrintingValue = true;
			Object value = deserializeValue(declField.getType());
			skipPrintingValue = false;

			if(value == null){
				PayloadDeserializer.appendToFile("\t" + declField.getType().getSimpleName());	
			}

			PayloadDeserializer.appendToFile("\t" + s.substring(s.lastIndexOf('.') + 1) + ".");
			PayloadDeserializer.appendToFile(declField.getName()+ "\t\t");
			//			PayloadDeserializer.appendToFile("\t\t" + declField.getType().getName() + "\ttab\t");
			PayloadDeserializer.appendToFile("\t" + (value == null ? "null" : value.toString()));
			//			PayloadDeserializer.appendToFile("###################################################\n\n");

			String fieldName = declField.getName();
			Method setter;
			/*
			 * If setters is non-null and there is a setter method for the given
			 * field, call the setter. Otherwise, set the field value directly. For
			 * persistence APIs such as JDO, the setter methods have been enhanced to
			 * manipulate additional object state, causing direct field writes to fail
			 * to update the object state properly.
			 */
			if ((setters != null) && ((setter = setters.get(fieldName)) != null)) {
				setter.invoke(instance, value);
			} else {
				boolean isAccessible = declField.isAccessible();
				boolean needsAccessOverride = !isAccessible && !Modifier.isPublic(declField.getModifiers());
				if (needsAccessOverride) {
					// Override access restrictions
					declField.setAccessible(true);
				}

				try{
					declField.set(instance, value);
				}catch(Exception e){
					PayloadDeserializer.appendToFile("\t%%%%%%%%%%   NOT ASSIGNABLE   %%%%%%%%%%%");
					PayloadDeserializer.appendToFile(declField.getName() + ": " + declField.getType().getSimpleName() +
							" cannot be set to " + value != null ? value.getClass().getSimpleName() : "null");
				}
				//				if(value == null || value.getClass().isAssignableFrom(declField.getType())){
				//					
				//				}else {
				//					
				//				}
			}
		}

		Class<?> superClass = instanceClass.getSuperclass();
		if (serializationPolicy.shouldDeserializeFields(superClass)) {
			deserializeImpl(SerializabilityUtil.hasCustomFieldSerializer(superClass), superClass, instance);
		}
	}

	private Object deserializeImpl(Class<?> customSerializer, Class<?> instanceClass, Object instance) 
	throws NoSuchMethodException, IllegalArgumentException, IllegalAccessException,
	InvocationTargetException, SerializationException, ClassNotFoundException {

		if (customSerializer != null) {
			@SuppressWarnings("unchecked")
			CustomFieldSerializer<Object> customFieldSerializer = (CustomFieldSerializer<Object>) SerializabilityUtil.loadCustomFieldSerializer(customSerializer);
			if (customFieldSerializer == null) {
				deserializeWithCustomFieldDeserializer(customSerializer, instanceClass, instance);
			} else {
				customFieldSerializer.deserializeInstance(this, instance);
			}
		} else if (instanceClass.isArray()) {
			instance = deserializeArray(instanceClass, instance);
		} else if (instanceClass.isEnum()) {
			// Enums are deserialized when they are instantiated
		} else {
			deserializeClass(instanceClass, instance);
		}

		return instance;
	}


	private void deserializeStringTable() throws SerializationException {
		int typeNameCount = readInt();

		BoundedList<String> buffer = new BoundedList<String>(String.class, typeNameCount);
		for (int typeNameIndex = 0; typeNameIndex < typeNameCount; ++typeNameIndex) {
			String str = extract();

			// Change quoted characters back.
			int idx = str.indexOf('\\');
			if (idx >= 0) {
				StringBuilder buf = new StringBuilder();
				int pos = 0;
				while (idx >= 0) {
					buf.append(str.substring(pos, idx));
					if (++idx == str.length()) {
						throw new SerializationException("Unmatched backslash: \"" + str + "\"");
					}
					char ch = str.charAt(idx);
					pos = idx + 1;
					switch (ch) {
					case '0':
						buf.append('\u0000');
						break;
					case '!':
						buf.append(RPC_SEPARATOR_CHAR);
						break;
					case '\\':
						buf.append(ch);
						break;
					case 'u':
						try {
							ch = (char) Integer.parseInt(str.substring(idx + 1, idx + 5), 16);
						} catch (NumberFormatException e) {
							throw new SerializationException("Invalid Unicode escape sequence in \"" + str + "\"");
						}
						buf.append(ch);
						pos += 4;
						break;
					default:
						throw new SerializationException("Unexpected escape character " + ch + " after backslash: \"" + str + "\"");
					}
					idx = str.indexOf('\\', pos);
				}
				buf.append(str.substring(pos));
				str = buf.toString();
			}
			buffer.add(str);
		}

		if (buffer.size() != buffer.getExpectedSize()) {
			throw new SerializationException("Expected " + buffer.getExpectedSize() + " string table elements; received " + buffer.size());
		}

		stringTable = buffer.toArray(new String[buffer.getExpectedSize()]);
	}

	private void deserializeWithCustomFieldDeserializer(Class<?> customSerializer, Class<?> instanceClass, Object instance)
	throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {

		assert (!instanceClass.isArray());

		for (Method method : customSerializer.getMethods()) {
			if ("deserialize".equals(method.getName())) {
				method.invoke(null, this, instance);
				return;
			}
		}
		throw new NoSuchMethodException("deserialize");
	}

	private String extract() throws SerializationException {
		try {
			String str = tokenListIndex == 0 ? "" : "\n";
			PayloadDeserializer.appendToFile(str + tokenListIndex);

			String tmpStr = tokenList.get(tokenListIndex++);
			PayloadDeserializer.appendToFile("\t=  " + tmpStr);

			return tmpStr;

		} catch (IndexOutOfBoundsException e) {
			throw new SerializationException("Too few tokens in RPC request", e);
		}
	}

	private String peek() throws SerializationException {
		try {
			String tmpStr = tokenList.get(tokenListIndex);
			return tmpStr;

		} catch (IndexOutOfBoundsException e) {
			throw new SerializationException("Too few tokens in RPC request", e);
		}
	}

	/**
	 * Returns a suitable NumberFormatException with an explanatory message
	 * when a numerical value cannot be parsed according to its expected
	 * type.
	 *  
	 * @param value the value as read from the RPC stream
	 * @param type the name of the expected type
	 * @param minValue the smallest valid value for the expected type
	 * @param maxValue the largest valid value for the expected type
	 * @return a NumberFormatException with an explanatory message
	 */
	private NumberFormatException getNumberFormatException(String value, String type, double minValue, double maxValue) {
		String message = "a non-numerical value";
		try {
			// Check the field contents in order to produce a more comprehensible
			// error message
			double d = Double.parseDouble(value);
			if (d < minValue || d > maxValue) {
				message = "an out-of-range value";
			} else if (d != Math.floor(d)) {
				message = "a fractional value";
			}
		} catch (NumberFormatException e2) {

		}

		return new NumberFormatException("Expected type '" + type + "' but received " + message + ": " + value);
	}

	/**
	 * Returns a Map from a field name to the setter method for that field, for a
	 * given class. The results are computed once for each class and cached.
	 * 
	 * @param instanceClass the class to query
	 */
	private Map<String, Method> getSetters(Class<?> instanceClass) {
		synchronized (settersByClass) {
			Map<String, Method> setters = settersByClass.get(instanceClass);
			if (setters == null) {
				setters = new HashMap<String, Method>();

				// Iterate over each field and locate a suitable setter method
				Field[] fields = instanceClass.getDeclaredFields();
				for (Field field : fields) {
					// Consider non-final, non-static, non-transient (or @GwtTransient) fields only
					if (SerializabilityUtil.isNotStaticTransientOrFinal(field)) {
						String fieldName = field.getName();
						String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
						try {
							Method setter = instanceClass.getMethod(setterName, field.getType());
							setters.put(fieldName, setter);
						} catch (NoSuchMethodException e) {
							// Just leave this field out of the map
						}
					}
				}

				settersByClass.put(instanceClass, setters);
			}

			return setters;
		}
	}

	@SuppressWarnings("rawtypes")
	private Object instantiate(Class<?> customSerializer, Class<?> instanceClass)
	throws InstantiationException, IllegalAccessException, IllegalArgumentException, 
	InvocationTargetException, NoSuchMethodException, SerializationException {
		if (customSerializer != null) {
			CustomFieldSerializer customFieldSerializer = SerializabilityUtil.loadCustomFieldSerializer(customSerializer);
			if (customFieldSerializer == null) {
				for (Method method : customSerializer.getMethods()) {
					if ("instantiate".equals(method.getName())) {
						return method.invoke(null, this);
					}
				}
				// Ok to not have one.
			} else if (customFieldSerializer.hasCustomInstantiateInstance()) {
				return customFieldSerializer.instantiateInstance(this);
			}
		}

		if (instanceClass.isArray()) {
			int length = readInt();
			PayloadDeserializer.appendToFile("\tarray size");
			// We don't pre-allocate the array; this prevents an allocation attack
			return new BoundedList<Object>(instanceClass.getComponentType(), length);
		} else if (instanceClass.isEnum()) {
			Enum<?>[] enumConstants = (Enum[]) instanceClass.getEnumConstants();
			int ordinal = readInt();
			assert (ordinal >= 0 && ordinal < enumConstants.length);
			return enumConstants[ordinal];
		} else {
			Constructor<?> constructor = instanceClass.getDeclaredConstructor();
			constructor.setAccessible(true);
			return constructor.newInstance();
		}
	}

	private void validateTypeVersions(Class<?> instanceClass, SerializedInstanceReference serializedInstRef) throws SerializationException {
		String clientTypeSignature = serializedInstRef.getSignature();
		if (clientTypeSignature.length() == 0) {
			throw new SerializationException("Missing type signature for " + instanceClass.getName());
		}

		String serverTypeSignature = SerializabilityUtil.getSerializationSignature(instanceClass, serializationPolicy);

		if (!clientTypeSignature.equals(serverTypeSignature)) {
			throw new SerializationException("Invalid type signature for " + instanceClass.getName());
		}
	}



	public ArrayList<String> getTokenList() {
		return tokenList;
	}

	public int getTokenListIndex() {
		return tokenListIndex;
	}

}
